using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
using Kaleida.ServiceMonitor.Framework;
using Kaleida.ServiceMonitor.Framework.Responses;

namespace Kaleida.ServiceMonitor.Operations.PreparedRequests
{
    public class TraceRoute : PreparedRequest
    {
        private readonly string hostName;
        private readonly TimeSpan timeout;

        public TraceRoute(string hostName) : this(hostName, "50s")
        {   
        }

        public TraceRoute(string hostName, string timeout)
        {
            if (hostName == null) throw new ArgumentNullException("hostName");

            var timeoutTimeSpan = timeout.ToTimeSpan();

            if (timeoutTimeSpan.TotalMilliseconds < 1)
                throw new ArgumentOutOfRangeException("timeout", timeout, "Cannot be less than 1 millisecond");

            this.hostName = hostName;
            this.timeout = timeoutTimeSpan;
        }

        public override string Description
        {
            get { return string.Format("Perform a traceroute to {0} (timeout set to {1}ms)", hostName, timeout.TotalMilliseconds); }
        }

        public override object GetResponse()
        {
            try
            {
                var traceRoute = new TraceRouteHelper(timeout);
                IList<IPAddress> ipAddresses = traceRoute.GetTraceRoute(hostName);

                return new TraceRouteResponse(ipAddresses);
            }
            catch (PingException exception)
            {
                return new TraceRouteResponse(exception);
            }
            catch (PingTimedOutException exception)
            {
                return new TraceRouteResponse(exception);
            }   
        }

        private class TraceRouteHelper
        {
            private const string Data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            private TimeSpan timeout;

            public TraceRouteHelper(TimeSpan timeout)
            {
                this.timeout = timeout;
            }

            public IList<IPAddress> GetTraceRoute(string hostNameOrAddress)
            {
                return GetTraceRoute(hostNameOrAddress, 1);
            }

            private IList<IPAddress> GetTraceRoute(string hostNameOrAddress, int ttl)
            {
                var pinger = new System.Net.NetworkInformation.Ping();
                var pingerOptions = new PingOptions(ttl, true);
                byte[] buffer = Encoding.ASCII.GetBytes(Data);

                PingReply reply = pinger.Send(hostNameOrAddress, (int)timeout.TotalMilliseconds, buffer, pingerOptions);
                if (reply == null)
                    throw new InvalidOperationException("Ping reply was null");

                var nodes = new List<IPAddress>();
                if (reply.Status == IPStatus.Success)
                {
                    nodes.Add(reply.Address);
                }
                else if (reply.Status == IPStatus.TtlExpired)
                {
                    nodes.Add(reply.Address);
                    nodes.AddRange(GetTraceRoute(hostNameOrAddress, ttl + 1));
                }
                else if (reply.Status == IPStatus.TimedOut)
                {
                    throw new PingTimedOutException(string.Format("Ping to {0} timed out after visiting {1} node(s)", hostNameOrAddress, nodes.Count));
                }
                else
                {
                    var route = string.Join(" -> ", nodes);
                    throw new InvalidOperationException(string.Format("Ping to {0} returned {1} after visiting {2} node(s): {3}", hostNameOrAddress, reply.Status, nodes.Count, route));
                }

                return nodes;
            }
        }
    }
}